/**
 * Copyright (C) 2021-2022 maminjie <canpool@163.com>
 * SPDX-License-Identifier: LGPL-2.1
 * Powered by Qt-Advanced-Docking-System
**/

#include "dockmanager.h"
#include "dockmanager_p.h"

#include "docksplitter.h"
#include "docktitlebar.h"
#include "dockpannel.h"
#include "dockfocuscontroller.h"
#include "dockstatereader.h"
#include "dockoverlay.h"
#include "dockwidget.h"
#include "docktab.h"
#include "dockfloatingwidget.h"
#include "dockiconprovider.h"

#include <algorithm>
#include <iostream>

#include <QAction>
#include <QApplication>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QList>
#include <QLoggingCategory>
#include <QMainWindow>
#include <QMap>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QVariant>
#include <QXmlStreamWriter>

#include <QAbstractButton>
#include <QPainter>
#include <QStyle>
#include <QVariant>
#include <QString>
#include <QWindow>

#ifdef Q_OS_LINUX
#include <QSettings>
#include <QFile>
#include <qpa/qplatformnativeinterface.h>
#endif

static void initResource()
{
//    Q_INIT_RESOURCE(QAds);
}

QADS_BEGIN_NAMESPACE
/**
 * Internal file version in case the structure changes internally
 */
enum StateFileVersion {
    InitialVersion = 0,       //!< InitialVersion
    Version1 = 1,             //!< Version1
    CurrentVersion = Version1 //!< CurrentVersion
};

namespace internal {

#ifdef Q_OS_LINUX
static QString _window_manager;
static QHash<QString, xcb_atom_t> _xcb_atom_cache;

//============================================================================
bool is_platform_x11()
{
    return QGuiApplication::platformName() == QLatin1String("xcb");
}

//============================================================================
xcb_connection_t *x11_connection()
{
    if (!qApp)
        return nullptr;
    QPlatformNativeInterface *native = qApp->platformNativeInterface();
    if (!native)
        return nullptr;

    void *connection = native->nativeResourceForIntegration(QByteArray("connection"));
    return reinterpret_cast<xcb_connection_t *>(connection);
}

//============================================================================
xcb_atom_t xcb_get_atom(const char *name)
{
    if (!is_platform_x11())
    {
        return XCB_ATOM_NONE;
    }
    auto key = QLatin1String(name);
    if(_xcb_atom_cache.contains(key))
    {
        return _xcb_atom_cache[key];
    }
    xcb_connection_t *connection = x11_connection();
    xcb_intern_atom_cookie_t request = xcb_intern_atom(connection, 1, strlen(name), name);
    xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(connection, request, nullptr);
    if (!reply)
    {
        return XCB_ATOM_NONE;
    }
    xcb_atom_t atom = reply->atom;
    if(atom == XCB_ATOM_NONE)
    {
        QADS_PRINT("Unknown Atom response from XServer: " << name);
    }
    else
    {
        _xcb_atom_cache.insert(key, atom);
    }
    free(reply);
    return atom;
}


//============================================================================
void xcb_update_prop(bool set, WId window, const char *type, const char *prop, const char *prop2)
{
    auto connection = x11_connection();
    xcb_atom_t type_atom = xcb_get_atom(type);
    xcb_atom_t prop_atom = xcb_get_atom(prop);
    xcb_client_message_event_t event;
    event.response_type = XCB_CLIENT_MESSAGE;
    event.format = 32;
    event.sequence = 0;
    event.window = window;
    event.type = type_atom;
    event.data.data32[0] = set ? 1 : 0;
    event.data.data32[1] = prop_atom;
    event.data.data32[2] = prop2 ? xcb_get_atom(prop2) : 0;
    event.data.data32[3] = 0;
    event.data.data32[4] = 0;

    xcb_send_event(connection, 0, window,
                   XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_PROPERTY_CHANGE,
                   (const char *)&event);
    xcb_flush(connection);
}


//============================================================================
xcb_get_property_reply_t* _xcb_get_props(WId window, const char *type, unsigned int atom_type)
{
    if (!is_platform_x11())
    {
        return nullptr;
    }
    xcb_connection_t *connection = x11_connection();
    xcb_atom_t type_atom = xcb_get_atom(type);
    if (type_atom == XCB_ATOM_NONE)
    {
        return nullptr;
    }
    xcb_get_property_cookie_t request = xcb_get_property_unchecked(connection, 0, window, type_atom, atom_type, 0, 1024);
    xcb_get_property_reply_t *reply = xcb_get_property_reply(connection, request, nullptr);
    if(reply && reply->type != atom_type)
    {
        QADS_PRINT("ATOM TYPE MISMATCH (" << type <<"). Expected: " << atom_type << "  but got " << reply->type);
        free(reply);
        return nullptr;
    }
    return reply;
}


//============================================================================
template <typename T>
void xcb_get_prop_list(WId window, const char *type, QVector<T> &ret, unsigned int atom_type)
{
    xcb_get_property_reply_t *reply = _xcb_get_props(window, type, atom_type);
    if (reply && reply->format == 32 && reply->type == atom_type && reply->value_len > 0)
    {
        const xcb_atom_t *data = static_cast<const T *>(xcb_get_property_value(reply));
        ret.resize(reply->value_len);
        memcpy((void *)&ret.first(), (void *)data, reply->value_len * sizeof(T));
    }
    free(reply);
}


//============================================================================
QString xcb_get_prop_string(WId window, const char *type)
{
    QString ret;
    // try utf8 first
    xcb_atom_t utf_atom = xcb_get_atom("UTF8_STRING");
    if(utf_atom != XCB_ATOM_NONE)
    {
        xcb_get_property_reply_t *reply = _xcb_get_props(window, type, utf_atom);
        if (reply && reply->format == 8 && reply->type == utf_atom)
        {
            const char *value = reinterpret_cast<const char *>(xcb_get_property_value(reply));
            ret = QString::fromUtf8(value, xcb_get_property_value_length(reply));
            free(reply);
            return ret;
        }
        free(reply);
    }
    // Fall back to XCB_ATOM_STRING
    xcb_get_property_reply_t *reply = _xcb_get_props(window, type, XCB_ATOM_STRING);
    if (reply && reply->format == 8 && reply->type == XCB_ATOM_STRING)
    {
        const char *value = reinterpret_cast<const char *>(xcb_get_property_value(reply));
        ret = QString::fromLatin1(value, xcb_get_property_value_length(reply));
    }
    free(reply);
    return ret;
}


//============================================================================
bool xcb_dump_props(WId window, const char *type)
{
    QVector<xcb_atom_t> atoms;
    xcb_get_prop_list(window, type, atoms, XCB_ATOM_ATOM);
    qDebug() << "\n\n!!!" << type << "  -  " << atoms.length();
    xcb_connection_t *connection = x11_connection();
    for (auto atom : atoms)
    {
        auto foo = xcb_get_atom_name(connection, atom);
        auto bar = xcb_get_atom_name_reply(connection, foo, nullptr);
        qDebug() << "\t" << xcb_get_atom_name_name(bar);
        free(bar);
    }
    return true;
}


//============================================================================
void xcb_add_prop(bool state, WId window, const char *type, const char *prop)
{
    if (!is_platform_x11())
    {
        return;
    }
    xcb_atom_t prop_atom = xcb_get_atom(prop);
    xcb_atom_t type_atom = xcb_get_atom(type);
    if (prop_atom == XCB_ATOM_NONE || type_atom == XCB_ATOM_NONE)
    {
        return;
    }
    QVector<xcb_atom_t> atoms;
    xcb_get_prop_list(window, type, atoms, XCB_ATOM_ATOM);
    int index = atoms.indexOf(prop_atom);
    if (state && index == -1)
    {
        atoms.push_back(prop_atom);
    }
    else if (!state && index >= 0)
    {
        atoms.remove(index);
    }
    xcb_connection_t *connection = x11_connection();
    xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, type_atom, XCB_ATOM_ATOM, 32, atoms.count(), atoms.constData());
    xcb_flush(connection);
}


//============================================================================
QString detectWindowManagerX11()
{
    // Tries to detect the windowmanager via X11.
    // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html#idm46018259946000
    if (!is_platform_x11())
    {
        return QLatin1String("UNKNOWN");
    }
    xcb_connection_t *connection = x11_connection();
    xcb_screen_t *first_screen = xcb_setup_roots_iterator (xcb_get_setup (connection)).data;
    if(!first_screen)
    {
        QADS_PRINT("No screen found via XCB.");
        return QLatin1String("UNKNOWN");
    }
    // Get supporting window ()
    xcb_window_t root = first_screen->root;
    xcb_window_t support_win = 0;
    QVector<xcb_window_t> sup_windows;
    xcb_get_prop_list(root, "_NET_SUPPORTING_WM_CHECK", sup_windows, XCB_ATOM_WINDOW);
    if(sup_windows.length() == 0)
    {
        // This doesn't seem to be in use anymore, but wmctrl does the same so lets play safe.
        // Both XCB_ATOM_CARDINAL and XCB_ATOM_WINDOW break down to a uint32_t, so reusing sup_windows should be fine.
        xcb_get_prop_list(root, "_WIN_SUPPORTING_WM_CHECK", sup_windows, XCB_ATOM_CARDINAL);
    }
    if(sup_windows.length() == 0)
    {
        QADS_PRINT("Failed to get the supporting window on non EWMH comform WM.");
        return QLatin1String("UNKNOWN");
    }
    support_win = sup_windows[0];
    QString ret = xcb_get_prop_string(support_win, "_NET_WM_NAME");
    if(ret.length() == 0)
    {
        QADS_PRINT("Empty WM name occured.");
        return QLatin1String("UNKNOWN");
    }
    return ret;
}

//============================================================================
QString windowManager()
{
    if(_window_manager.length() == 0)
    {
        _window_manager = detectWindowManagerX11();
    }
    return _window_manager;
}
#endif

void replaceSplitterWidget(QSplitter *splitter, QWidget *from, QWidget *to)
{
    int index = splitter->indexOf(from);
    from->setParent(nullptr);
    splitter->insertWidget(index, to);
}

QPixmap createTransparentPixmap(const QPixmap &source, qreal opacity)
{
    QPixmap transparentPixmap(source.size());
    transparentPixmap.fill(Qt::transparent);
    QPainter painter(&transparentPixmap);
    painter.setOpacity(opacity);
    painter.drawPixmap(0, 0, source);
    return transparentPixmap;
}

void hideEmptyParentSplitters(DockSplitter *splitter)
{
    while (splitter && splitter->isVisible()) {
        if (!splitter->hasVisibleContent()) {
            splitter->hide();
        }
        splitter = internal::findParent<DockSplitter *>(splitter);
    }
}

void setButtonIcon(QAbstractButton *button, QStyle::StandardPixmap standarPixmap, QAds::IconRole role)
{
    // First we try to use custom icons if available
    QIcon icon = DockManager::iconProvider().customIcon(role);

    if (!icon.isNull()) {
        button->setIcon(icon);
        return;
    }

#if defined(Q_OS_LINUX)
    button->setIcon(button->style()->standardIcon(standarPixmap));
#else
    // The standard icons does not look good on high DPI screens so we create
    // our own "standard" icon here.
    QPixmap normalPixmap = button->style()->standardPixmap(standarPixmap, nullptr, button);
    icon.addPixmap(internal::createTransparentPixmap(normalPixmap, 0.25), QIcon::Disabled);
    icon.addPixmap(normalPixmap, QIcon::Normal);
    button->setIcon(icon);
#endif
}

void repolishStyle(QWidget *widget, RepolishChildOptions options)
{
    if (!widget) {
        return;
    }

    widget->style()->unpolish(widget);
    widget->style()->polish(widget);

    if (options == RepolishIgnoreChildren) {
        return;
    }

    Qt::FindChildOptions opt = (options == RepolishDirectChildren) ? Qt::FindDirectChildrenOnly
                                                                   : Qt::FindChildrenRecursively;
    QList<QWidget *> children = widget->findChildren<QWidget *>(QString(), opt);
    for (auto w : qAsConst(children)) {
        w->style()->unpolish(w);
        w->style()->polish(w);
    }
}

} // namespace internal

class DockManagerPrivate
{
public:
    DockManagerPrivate(DockManager *parent);

    bool restoreStateFromXml(const QByteArray &state, int version, bool testing = false);
    bool restoreState(const QByteArray &state, int version);
    void restoreDockWidgetsOpenState();
    void restoreDockPannelsIndices();
    bool restoreContainer(int index, DockStateReader &stream, bool testing);
    void emitTopLevelEvents();

    void hideFloatingWidgets();
    void markDockWidgetsDirty();

    void addActionToMenu(QAction *action, QMenu *menu, bool insertSorted);
    void loadStylesheet();

public:
    DockManager *q;
    QList<QPointer<DockFloatingWidget> > m_floatingWidgets;
    QList<DockFloatingWidget *> m_hiddenFloatingWidgets;
    QList<DockContainer *> m_containers;
    DockWidget *m_centralWidget = nullptr;
    DockOverlay *m_containerOverlay = nullptr;
    DockOverlay *m_pannelOverlay = nullptr;
    QMap<QString, DockWidget *> m_dockWidgetsMap;
    QMap<QString, QByteArray> m_perspectives;
    QMap<QString, QMenu *> m_viewMenuGroups;
    QMenu * m_viewMenu;
    DockManager::ViewMenuInsertionOrder m_menuInsertionOrder = DockManager::MenuAlphabeticallySorted;
    QVector<DockFloatingWidget *> m_uninitializedFloatingWidgets;
    DockFocusController *m_focusController = nullptr;
    bool m_restoringState = false;
    bool m_modeChangeState = false;
}; // class DockManagerPrivate

DockManagerPrivate::DockManagerPrivate(DockManager *parent)
    : q(parent)
{}

bool DockManagerPrivate::restoreContainer(int index, DockStateReader &stream, bool testing)
{
    if (testing) {
        index = 0;
    }
    bool result = false;

    if (index >= m_containers.count()) {
        DockFloatingWidget *floatingWidget = new DockFloatingWidget(q);
        result = floatingWidget->restoreState(stream, testing);
    } else {
        auto container = m_containers[index];
        if (container->isFloating()) {
            result = container->floatingWidget()->restoreState(stream, testing);
        } else {
            result = container->restoreState(stream, testing);
        }
    }

    return result;
}

/**
 * Restores the state. If testing is set to true it will check if
 * the given data stream is a valid docking system state file.
 */
bool DockManagerPrivate::restoreStateFromXml(const QByteArray &state, int version, bool testing)
{
    Q_UNUSED(version) // TODO version is not needed, why is it in here in the first place?

    if (state.isEmpty()) {
        return false;
    }

    DockStateReader stateReader(state);
    if (!stateReader.readNextStartElement()) {
        return false;
    }
    if (stateReader.name() != QLatin1String("QtAdvancedDockingSystem")) {
        return false;
    }

    bool ok;
    int v = stateReader.attributes().value("version").toInt(&ok);
    if (!ok || v > CurrentVersion) {
        return false;
    }

    stateReader.setFileVersion(v);

    // Older files do not support UserVersion but we still want to load them so
    // we first test if the attribute exists
    if (!stateReader.attributes().value("userVersion").isEmpty()) {
        v = stateReader.attributes().value("userVersion").toInt(&ok);
        if (!ok || v != version) {
            return false;
        }
    }

    if (m_centralWidget) {
        const auto centralWidgetAttribute = stateReader.attributes().value("centralWidget");
        // If we have a central widget but a state without central widget, then
        // something is wrong.
        if (centralWidgetAttribute.isEmpty()) {
            return false;
        }

        // If the object name of the central widget does not match the name of the
        // saved central widget, the something is wrong
        if (m_centralWidget->objectName() != centralWidgetAttribute.toString()) {
            return false;
        }
    }

    bool result = true;
    int dockContainerCount = 0;

    while (stateReader.readNextStartElement()) {
        if (stateReader.name() == QLatin1String("container")) {
            result = restoreContainer(dockContainerCount, stateReader, testing);
            if (!result) {
                break;
            }
            dockContainerCount++;
        }
    }

    if (!testing) {
        // Delete remaining empty floating widgets
        int floatingWidgetIndex = dockContainerCount - 1;
        int deleteCount = m_floatingWidgets.count() - floatingWidgetIndex;

        for (int i = 0; i < deleteCount; ++i) {
            m_floatingWidgets[floatingWidgetIndex + i]->deleteLater();
            q->removeDockContainer(m_floatingWidgets[floatingWidgetIndex + i]->dockContainer());
        }
    }

    return result;
}

void DockManagerPrivate::restoreDockWidgetsOpenState()
{
    // All dock widgets, that have not been processed in the restore state
    // function are invisible to the user now and have no assigned dock area
    // They do not belong to any dock container, until the user toggles the
    // toggle view action the next time
    for (auto dockWidget : m_dockWidgetsMap) {
        if (dockWidget->property(internal::dirtyProperty).toBool()) {
            dockWidget->flagAsUnassigned();
            emit dockWidget->viewToggled(false);
        } else {
            dockWidget->toggleViewInternal(!dockWidget->property(internal::closedProperty).toBool());
        }
    }
}

void DockManagerPrivate::restoreDockPannelsIndices()
{
    // Now all dock pannels are properly restored and we setup the index of
    // The dock pannels because the previous toggleView() action has changed
    // the dock pannels index
    int count = 0;

    for (auto dockContainer : qAsConst(m_containers)) {
        count++;

        for (int i = 0; i < dockContainer->dockPannelCount(); ++i) {
            DockPannel *pannel = dockContainer->dockPannel(i);
            QString dockWidgetName = pannel->property("currentDockWidget").toString();
            DockWidget *dockWidget = nullptr;

            if (!dockWidgetName.isEmpty()) {
                dockWidget = q->findDockWidget(dockWidgetName);
            }

            if (!dockWidget || dockWidget->isClosed()) {
                int index = pannel->indexOfFirstOpenDockWidget();
                if (index < 0) {
                    continue;
                }
                pannel->setCurrentIndex(index);
            } else {
                pannel->setCurrentDockWidgetInternal(dockWidget);
            }
        }
    }
}

void DockManagerPrivate::emitTopLevelEvents()
{
    // Finally we need to send the topLevelChanged() signals for all dock
    // widgets if top level changed
    for (auto dockContainer : qAsConst(m_containers)) {
        DockWidget *topLevelDockWidget = dockContainer->topLevelDockWidget();
        if (topLevelDockWidget) {
            topLevelDockWidget->emitTopLevelChanged(true);
        } else {
            for (int i = 0; i < dockContainer->dockPannelCount(); ++i) {
                auto pannel = dockContainer->dockPannel(i);
                for (auto dockWidget : pannel->dockWidgets()) {
                    dockWidget->emitTopLevelChanged(false);
                }
            }
        }
    }
}

void DockManagerPrivate::hideFloatingWidgets()
{
    // Hide updates of floating widgets from user
    for (auto floatingWidget : qAsConst(m_floatingWidgets)) {
        floatingWidget->hide();
    }
}

void DockManagerPrivate::markDockWidgetsDirty()
{
    for (auto dockWidget : qAsConst(m_dockWidgetsMap)) {
        dockWidget->setProperty("dirty", true);
    }
}

void DockManagerPrivate::addActionToMenu(QAction *action, QMenu *menu, bool insertSorted)
{
    if (insertSorted) {
        auto actions = menu->actions();
        auto it = std::find_if(actions.begin(), actions.end(),
            [&action](const QAction *a) {
                return a->text().compare(action->text(), Qt::CaseInsensitive) > 0;
            });

        if (it == actions.end()) {
            menu->addAction(action);
        } else {
            menu->insertAction(*it, action);
        }
    } else {
        menu->addAction(action);
    }
}

void DockManagerPrivate::loadStylesheet()
{
    initResource();
    QString result;
    QString fileName = ":ads/stylesheets/";
    fileName += QAds::testConfigFlag(QAds::FocusHighlighting)
        ? "focus_highlighting" : "default";
#ifdef Q_OS_LINUX
    fileName += "_linux";
#endif
    fileName += ".css";
    QFile styleSheetFile(fileName);
    styleSheetFile.open(QIODevice::ReadOnly);
    QTextStream StyleSheetStream(&styleSheetFile);
    result = StyleSheetStream.readAll();
    styleSheetFile.close();
    q->setStyleSheet(result);
}

bool DockManagerPrivate::restoreState(const QByteArray &state, int version)
{
    QByteArray currentState = state.startsWith("<?xml") ? state : qUncompress(state);

    // Check the format of the given data stream
    if (!restoreStateFromXml(currentState, version, true)) {
        return false;
    }

    // Hide updates of floating widgets from use
    hideFloatingWidgets();
    markDockWidgetsDirty();

    if (!restoreStateFromXml(currentState, version)) {
        return false;
    }

    restoreDockWidgetsOpenState();
    restoreDockPannelsIndices();
    emitTopLevelEvents();
    return true;
}

DockManager::DockManager(QWidget *parent)
    : DockContainer(this, parent)
    , d(new DockManagerPrivate(this))
{
    createRootSplitter();
    QMainWindow *mainWindow = qobject_cast<QMainWindow*>(parent);
    if (mainWindow) {
        mainWindow->setCentralWidget(this);
    }
    d->m_viewMenu = new QMenu(tr("Show View"), this);
    d->m_pannelOverlay = new DockOverlay(this, DockOverlay::ModePannelOverlay);
    d->m_containerOverlay = new DockOverlay(this, DockOverlay::ModeContainerOverlay);
    d->m_containers.append(this);
    d->loadStylesheet();

    if (QAds::testConfigFlag(QAds::FocusHighlighting)) {
        d->m_focusController = new DockFocusController(this);
    }
#ifdef Q_OS_LINUX
    window()->installEventFilter(this);
    connect(qApp, &QApplication::focusWindowChanged, [](QWindow* focusWindow)
    {
        // bring modal dialogs to foreground to ensure that they are in front of any
        // floating dock widget
        if (focusWindow && focusWindow->isModal()) {
            focusWindow->raise();
        }
    });
#endif
}

DockManager::~DockManager()
{
    // fix memory leaks, see https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/307
    std::vector<DockPannel *> pannels;
    for (int i = 0; i != dockPannelCount(); ++i) {
        pannels.push_back(dockPannel(i));
    }
    for (auto pannel : pannels) {
        for (auto widget : pannel->dockWidgets()) {
            delete widget;
        }
        delete pannel;
    }

    for (auto floatingWidget : qAsConst(d->m_floatingWidgets)) {
        /* There have been crashes with partially destructed widgets in
           m_floatingWidgets. Those do not have a parent. */
        if (floatingWidget && floatingWidget->parent() == this)
            delete floatingWidget.data();
    }
    d->m_floatingWidgets.clear();
    delete d;
}

DockPannel *DockManager::addDockWidget(DockWidgetArea area, DockWidget *dockWidget, DockPannel *targetPannel)
{
    d->m_dockWidgetsMap.insert(dockWidget->objectName(), dockWidget);
    auto container = targetPannel ? targetPannel->dockContainer() : this;
    auto pannel = container->addDockWidget(area, dockWidget, targetPannel);
    emit dockWidgetAdded(dockWidget);
    return pannel;
//    return DockContainer::addDockWidget(area, dockWidget, targetPannel);
}

DockPannel *DockManager::addDockWidget(DockWidgetArea area, DockWidget *dockWidget)
{
    DockPannel *pannel = lastAddedDockPannel(area);
    if (pannel) {
        return addDockWidget(QAds::CenterDockWidgetArea, dockWidget, pannel);
    } else {
        return addDockWidget(area, dockWidget, nullptr);
    }
}

DockPannel *DockManager::addDockWidget(DockWidget *dockWidget, DockPannel *targetPannel)
{
    return addDockWidget(QAds::CenterDockWidgetArea, dockWidget, targetPannel);
}

/**
 * Adds dockwidget into the given container.
 * This allows you to place the dock widget into a container, even if that
 * container does not yet contain a DockPannel.
 * \return Returns the dock area widget that contains the new DockWidget
 */
DockPannel *DockManager::addDockWidgetToContainer(DockWidgetArea area, DockWidget *dockwidget, DockContainer *container)
{
    d->m_dockWidgetsMap.insert(dockwidget->objectName(), dockwidget);
    auto pannel = container->addDockWidget(area, dockwidget);
    Q_EMIT dockWidgetAdded(dockwidget);
    return pannel;
}

DockWidget *DockManager::centralWidget() const
{
    return d->m_centralWidget;
}

DockPannel *DockManager::setCentralWidget(DockWidget *widget)
{
    if (!widget) {
        d->m_centralWidget = nullptr;
        return nullptr;
    }

    // Setting a new central widget is now allowed if there is already a central
    // widget or if there are already other dock widgets
    if (d->m_centralWidget) {
        qWarning("Setting a central widget not possible because there is already a central widget.");
        return nullptr;
    }

    // Setting a central widget is now allowed if there are already other
    // dock widgets.
    if (!d->m_dockWidgetsMap.isEmpty()) {
        qWarning("Setting a central widget not possible - the central widget need to be the first "
            "dock widget that is added to the dock manager.");
        return nullptr;
    }

    widget->setFeature(DockWidget::DockWidgetClosable, false);
    widget->setFeature(DockWidget::DockWidgetMovable, false);
    widget->setFeature(DockWidget::DockWidgetFloatable, false);
    d->m_centralWidget = widget;
    DockPannel* centralPannel = addDockWidget(CenterDockWidgetArea, widget);
    centralPannel->setDockPannelFlag(DockPannel::DockPannelFlag::HideSingleWidgetTitleBar, true);
    return centralPannel;
}

QMap<QString, DockWidget *> DockManager::dockWidgetsMap() const
{
    return d->m_dockWidgetsMap;
}

DockWidget *DockManager::findDockWidget(const QString &objectName) const
{
    return d->m_dockWidgetsMap.value(objectName, nullptr);
}

void DockManager::removeDockWidget(DockWidget *dockWidget)
{
    emit dockWidgetAboutToBeRemoved(dockWidget);
    d->m_dockWidgetsMap.remove(dockWidget->objectName());
    DockContainer::removeDockWidget(dockWidget);
    dockWidget->setDockManager(nullptr);
    emit dockWidgetRemoved(dockWidget);
}

void DockManager::setDockWidgetFocused(DockWidget *dockWidget)
{
    if (d->m_focusController) {
        d->m_focusController->setDockWidgetFocused(dockWidget);
    }
}

void DockManager::restoreHiddenFloatingWidgets()
{
    if (d->m_hiddenFloatingWidgets.isEmpty()) {
        return;
    }
    // Restore floating widgets that were hidden upon hideManagerAndFloatingWidgets
    for (auto floatingWidget : d->m_hiddenFloatingWidgets) {
        bool hasDockWidgetVisible = false;

        // Needed to prevent CFloatingDockContainer being shown empty
        // Could make sense to move this to CFloatingDockContainer::showEvent(QShowEvent *event)
        // if experiencing CFloatingDockContainer being shown empty in other situations, but let's keep
        // it here for now to make sure changes to fix Issue #380 does not impact existing behaviours
        for (auto dockWidget : floatingWidget->dockWidgets()) {
            if (dockWidget->toggleViewAction()->isChecked()) {
                dockWidget->toggleView(true);
                hasDockWidgetVisible = true;
            }
        }

        if (hasDockWidgetVisible) {
            floatingWidget->show();
        }
    }
    d->m_hiddenFloatingWidgets.clear();
}

DockFloatingWidget *DockManager::addDockWidgetFloating(DockWidget *dockWidget)
{
    d->m_dockWidgetsMap.insert(dockWidget->objectName(), dockWidget);
    DockPannel *oldPannel = dockWidget->dockPannel();

    if (oldPannel) {
        oldPannel->removeDockWidget(dockWidget);
    }

    dockWidget->setDockManager(this);
    DockFloatingWidget *floatingWidget = new DockFloatingWidget(dockWidget);
    floatingWidget->resize(dockWidget->size());

    if (isVisible()) {
        floatingWidget->show();
    } else {
        d->m_uninitializedFloatingWidgets.append(floatingWidget);
    }

    return floatingWidget;
}

const QList<QPointer<DockFloatingWidget> > DockManager::floatingWidgets() const
{
    return d->m_floatingWidgets;
}

const QList<DockContainer *> DockManager::dockContainers() const
{
    return d->m_containers;
}

/**
 * Adds a toggle view action to the the internal view menu.
 * You can either manage the insertion of the toggle view actions in your
 * application or you can add the actions to the internal view menu and
 * then simply insert the menu object into your.
 * \param[in] ToggleViewAction The action to insert. If no group is provided
 *            the action is directly inserted into the menu. If a group
 *            is provided, the action is inserted into the group and the
 *            group is inserted into the menu if it is not existing yet.
 * \param[in] Group This is the text used for the group menu item
 * \param[in] GroupIcon The icon used for grouping the workbenches in the
 *            view menu. I.e. if there is a workbench for each device
 *            like for spectrometer devices, it is good to group all these
 *            workbenches under a menu item
 * \return If Group is not empty, this function returns the GroupAction
 *         for this group. If the group is empty, the function returns
 *         the given ToggleViewAction.
 */
QAction *DockManager::addToggleViewActionToMenu(QAction *toggleViewAction, const QString &group,
                                                const QIcon &groupIcon)
{
    bool alphabeticallySorted = (MenuAlphabeticallySorted == d->m_menuInsertionOrder);
    if (!group.isEmpty()) {
        QMenu* groupMenu = d->m_viewMenuGroups.value(group, 0);
        if (!groupMenu) {
            groupMenu = new QMenu(group, this);
            groupMenu->setIcon(groupIcon);
            d->addActionToMenu(groupMenu->menuAction(), d->m_viewMenu, alphabeticallySorted);
            d->m_viewMenuGroups.insert(group, groupMenu);
        } else if (groupMenu->icon().isNull() && !groupIcon.isNull()) {
            groupMenu->setIcon(groupIcon);
        }

        d->addActionToMenu(toggleViewAction, groupMenu, alphabeticallySorted);
        return groupMenu->menuAction();
    } else {
        d->addActionToMenu(toggleViewAction, d->m_viewMenu, alphabeticallySorted);
        return toggleViewAction;
    }
}

/**
 * This function returns the internal view menu.
 * To fill the view menu, you can use the addToggleViewActionToMenu()
 * function.
 */
QMenu *DockManager::viewMenu() const
{
    return d->m_viewMenu;
}

/**
 * Define the insertion order for toggle view menu items.
 * The order defines how the actions are added to the view menu.
 * The default insertion order is MenuAlphabeticallySorted to make it
 * easier for users to find the menu entry for a certain dock widget.
 * You need to call this function befor you insert the first menu item
 * into the view menu.
 */
void DockManager::setViewMenuInsertionOrder(DockManager::ViewMenuInsertionOrder order)
{
    d->m_menuInsertionOrder = order;
}

/**
 * Returns the dock widget that has focus style in the ui or a nullptr if
 * not dock widget is painted focused.
 * If the flag FocusHighlighting is disabled, this function always returns
 * nullptr.
 */
DockWidget *DockManager::focusedDockWidget() const
{
    if (!d->m_focusController) {
        return nullptr;
    } else {
        return d->m_focusController->focusedDockWidget();
    }
}

/**
 * Returns the sizes of the splitter that contains the dock pannel.
 *
 * If there is no splitter that contains the pannel, an empty list will be
 * returned.
 */
QList<int> DockManager::splitterSizes(DockPannel *pannel) const
{
    if (pannel) {
        auto splitter = internal::findParent<DockSplitter *>(pannel);
        if (splitter) {
            return splitter->sizes();
        }
    }
    return QList<int>();
}

/**
 * Update the sizes of a splitter
 * Programmatically updates the sizes of a given splitter by calling
 * QSplitter::setSizes(). The splitter will be the splitter that
 * contains the supplied dock pannel widget. If there is not splitter
 * that contains the dock pannel, or the sizes supplied does not match
 * the number of children of the splitter, this method will have no
 * effect.
 */
void DockManager::setSplitterSizes(DockPannel *pannel, const QList<int> &sizes)
{
    if (pannel == nullptr) {
        return;
    }
    auto splitter = internal::findParent<DockSplitter *>(pannel);
    if (splitter && splitter->count() == sizes.count()) {
        splitter->setSizes(sizes);
    }
}

/**
 * Saves the current perspective to the internal list of perspectives.
 * A perspective is the current state of the dock manager assigned
 * with a certain name. This makes it possible for the user,
 * to switch between different perspectives quickly.
 * If a perspective with the given name already exists, then
 * it will be overwritten with the new state.
 */
void DockManager::addPerspective(const QString &uniquePrespectiveName)
{
    d->m_perspectives.insert(uniquePrespectiveName, saveState());
    emit perspectiveListChanged();
}

/**
 * Removes the perspective with the given name from the list of perspectives
 */
void DockManager::removePerspective(const QString &name)
{
    removePerspectives({name});
}

/**
 * Removes the given perspectives from the dock manager
 */
void DockManager::removePerspectives(const QStringList &names)
{
    int count = 0;
    for (auto name : names) {
        count += d->m_perspectives.remove(name);
    }

    if (count) {
        emit perspectivesRemoved();
        emit perspectiveListChanged();
    }
}

/**
 * Returns the names of all available perspectives
 */
QStringList DockManager::perspectiveNames() const
{
    return d->m_perspectives.keys();
}

/**
 * Opens the perspective with the given name.
 */
void DockManager::openPerspective(const QString &perspectiveName)
{
    const auto iterator = d->m_perspectives.find(perspectiveName);
    if (d->m_perspectives.end() == iterator) {
        return;
    }

    emit openingPerspective(perspectiveName);
    restoreState(iterator.value());
    emit perspectiveOpened(perspectiveName);
}

/**
 * Saves the perspectives to the given settings file.
 */
void DockManager::savePerspectives(QSettings &settings) const
{
    settings.beginWriteArray("perspectives", d->m_perspectives.size());
    int i = 0;
    for (auto it = d->m_perspectives.constBegin(); it != d->m_perspectives.constEnd(); ++it) {
        settings.setArrayIndex(i);
        settings.setValue("name", it.key());
        settings.setValue("state", it.value());
        ++i;
    }
    settings.endArray();
}

/**
 * Loads the perspectives from the given settings file
 */
void DockManager::loadPerspectives(QSettings &settings)
{
    d->m_perspectives.clear();
    int size = settings.beginReadArray("perspectives");
    if (!size) {
        settings.endArray();
        return;
    }

    for (int i = 0; i < size; ++i) {
        settings.setArrayIndex(i);
        QString name = settings.value("name").toString();
        QByteArray data = settings.value("state").toByteArray();
        if (name.isEmpty() || data.isEmpty()) {
            continue;
        }

        d->m_perspectives.insert(name, data);
    }

    settings.endArray();
    Q_EMIT perspectiveListChanged();
    Q_EMIT perspectiveListLoaded();
}

unsigned int DockManager::zOrderIndex() const
{
    return 0;
}

QByteArray DockManager::saveState(int version) const
{
    QByteArray xmlData;
    QXmlStreamWriter stream(&xmlData);
    stream.setAutoFormatting(QAds::testConfigFlag(XmlAutoFormattingEnabled));
    stream.writeStartDocument();
    stream.writeStartElement("QtAdvancedDockingSystem");
    stream.writeAttribute("version", QString::number(CurrentVersion));
    stream.writeAttribute("userVersion", QString::number(version));
    stream.writeAttribute("containers", QString::number(d->m_containers.count()));
    if (d->m_centralWidget) {
        stream.writeAttribute("centralWidget", d->m_centralWidget->objectName());
    }

    for (auto container : qAsConst(d->m_containers)) {
        container->saveState(stream);
    }

    stream.writeEndElement();
    stream.writeEndDocument();
    return QAds::testConfigFlag(XmlCompressionEnabled) ? qCompress(xmlData, 9) : xmlData;
}

bool DockManager::restoreState(const QByteArray &state, int version)
{
    // Prevent multiple calls as long as state is not restore. This may
    // happen, if QApplication::processEvents() is called somewhere
    if (d->m_restoringState) {
        return false;
    }

    // We hide the complete dock manager here. Restoring the state means
    // that DockWidgets are removed from the DockArea internal stack layout
    // which in turn  means, that each time a widget is removed the stack
    // will show and raise the next available widget which in turn
    // triggers show events for the dock widgets. To avoid this we hide the
    // dock manager. Because there will be no processing of application
    // events until this function is finished, the user will not see this
    // hiding
    bool isHidden = this->isHidden();
    if (!isHidden) {
        hide();
    }

    d->m_restoringState = true;
    emit restoringState();
    bool result = d->restoreState(state, version);
    d->m_restoringState = false;

    if (!isHidden) {
        show();
    }

    emit stateRestored();
    return result;
}

bool DockManager::isRestoringState() const
{
    return d->m_restoringState;
}

DockIconProvider &DockManager::iconProvider()
{
    static DockIconProvider instance;
    return instance;
}

int DockManager::startDragDistance()
{
    return static_cast<int>(QApplication::startDragDistance() * 1.5);
}

void DockManager::registerFloatingWidget(DockFloatingWidget *floatingWidget)
{
    d->m_floatingWidgets.append(floatingWidget);
    emit floatingWidgetCreated(floatingWidget);
}

void DockManager::removeFloatingWidget(DockFloatingWidget *floatingWidget)
{
    d->m_floatingWidgets.removeAll(floatingWidget);
}

void DockManager::registerDockContainer(DockContainer *dockContainer)
{
    d->m_containers.append(dockContainer);
}

void DockManager::removeDockContainer(DockContainer *dockContainer)
{
    if (this != dockContainer) {
        d->m_containers.removeAll(dockContainer);
    }
}

DockOverlay *DockManager::containerOverlay() const
{
    return d->m_containerOverlay;
}

DockOverlay *DockManager::pannelOverlay() const
{
    return d->m_pannelOverlay;
}

void DockManager::notifyWidgetOrPannelRelocation(QWidget *droppedWidget)
{
    if (d->m_focusController) {
        d->m_focusController->notifyWidgetOrPannelRelocation(droppedWidget);
    }
}

void DockManager::notifyFloatingWidgetDrop(DockFloatingWidget *floatingWidget)
{
    if (d->m_focusController) {
        d->m_focusController->notifyFloatingWidgetDrop(floatingWidget);
    }
}

DockFocusController *DockManager::dockFocusController() const
{
    return d->m_focusController;
}

void DockManager::hideManagerAndFloatingWidgets()
{
    hide();

    d->m_hiddenFloatingWidgets.clear();
    // Hide updates of floating widgets from user
    for (auto floatingWidget : d->m_floatingWidgets) {
        if (floatingWidget->isVisible()) {
            QList<DockWidget *> visibleWidgets;
            for (auto dockWidget : floatingWidget->dockWidgets()) {
                if (dockWidget->toggleViewAction()->isChecked())
                    visibleWidgets.push_back(dockWidget);
            }

            // save as floating widget to be shown when CDockManager will be shown back
            d->m_hiddenFloatingWidgets.push_back(floatingWidget);
            floatingWidget->hide();

            // hidding floating widget automatically marked contained CDockWidgets as hidden
            // but they must remain marked as visible as we want them to be restored visible
            // when CDockManager will be shown back
            for (auto dockWidget : visibleWidgets) {
                dockWidget->toggleViewAction()->setChecked(true);
            }
        }
    }
}

void DockManager::showEvent(QShowEvent *event)
{
    Super::showEvent(event);

    // Fix Issue #380
    restoreHiddenFloatingWidgets();

    if (d->m_uninitializedFloatingWidgets.empty()) {
        return;
    }

    for (auto floatingWidget : qAsConst(d->m_uninitializedFloatingWidgets)) {
        // Check, if someone closed a floating dock widget before the dock manager is shown
        if (floatingWidget->dockContainer()->hasOpenDockPannels()) {
            floatingWidget->show();
        }
    }

    d->m_uninitializedFloatingWidgets.clear();
}

#ifdef Q_OS_LINUX
bool DockManager::eventFilter(QObject *obj, QEvent *e)
{
    // Emulate Qt:Tool behaviour.
    // Required because on some WMs Tool windows can't be maximized.

    // Window always on top of the MainWindow.
    if (e->type() == QEvent::WindowActivate)
    {
        for (auto _window : floatingWidgets())
        {
            if (!_window->isVisible() || window()->isMinimized())
            {
                continue;
            }
            // setWindowFlags(Qt::WindowStaysOnTopHint) will hide the window and thus requires a show call.
            // This then leads to flickering and a nasty endless loop (also buggy behaviour on Ubuntu).
            // So we just do it ourself.
            if (QGuiApplication::platformName() == QLatin1String("xcb"))
            {
                internal::xcb_update_prop(true, _window->window()->winId(),
                    "_NET_WM_STATE", "_NET_WM_STATE_ABOVE", "_NET_WM_STATE_STAYS_ON_TOP");
            }
            else
            {
                _window->setWindowFlag(Qt::WindowStaysOnTopHint, true);
            }
        }
    }
    else if (e->type() == QEvent::WindowDeactivate)
    {
        for (auto _window : floatingWidgets())
        {
            if (!_window->isVisible() || window()->isMinimized())
            {
                continue;
            }
            if (QGuiApplication::platformName() == QLatin1String("xcb"))
            {
                internal::xcb_update_prop(false, _window->window()->winId(),
                    "_NET_WM_STATE", "_NET_WM_STATE_ABOVE", "_NET_WM_STATE_STAYS_ON_TOP");
            }
            else
            {
                _window->setWindowFlag(Qt::WindowStaysOnTopHint, false);
            }
            _window->raise();
        }
    }

    // Sync minimize with MainWindow
    if (e->type() == QEvent::WindowStateChange)
    {
        for (auto _window : floatingWidgets())
        {
            if (! _window->isVisible())
            {
                continue;
            }

            if (window()->isMinimized())
            {
                _window->showMinimized();
            }
            else
            {
                _window->setWindowState(_window->windowState() & (~Qt::WindowMinimized));
            }
        }
        if (!window()->isMinimized())
        {
            QApplication::setActiveWindow(window());
        }
    }
    return Super::eventFilter(obj, e);
}
#endif

QADS_END_NAMESPACE
